#region Using Statements
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Collections;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using MorphTarget;

using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;
//using TOutput = Microsoft.Xna.Framework.Content.Pipeline.Processors.ModelContent;
using TOutput = System.Collections.Generic.List<MorphTarget.MorphInfo>;
#endregion

namespace MorphImporter
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// </summary>
    [ContentProcessor(DisplayName = "MorphImporter::Processor")]
    public class MorphProcessor : ContentProcessor<TInput, TOutput> //ModelProcessor
    {
        #region Content Properties
        private string baseName = String.Empty;
        private string morphName = String.Empty;

        private float scaleVal = 1.0f;
        private float rotationXVal = 0.0f;
        private float rotationYVal = 0.0f;
        private float rotationZVal = 0.0f;

        [DisplayName("Morph Name"),
        Description("String identifier of the morph"),
        DefaultValue(typeof(string), "")]
        public virtual string MorphName
        {
            get { return morphName; }
            set { morphName = value; }
        }
        [DisplayName("Base Mesh"),
        Description("Name of base model to compare with, preferrably of the same mesh type. If this is a zero mesh, use the original model."),
        DefaultValue(typeof(string), "")]
        public virtual string BaseName
        {
            get { return baseName; }
            set { baseName = value; }
        }

        [DisplayName("Scale"),
        Description("Scale model by given factor."),
        DefaultValue(typeof(float), "1.0")]
        public virtual float Scale
        {
            get { return scaleVal; }
            set { scaleVal = value; }
        }
        [DisplayName("X Rotation"),
        Description("Rotate model along X."),
        DefaultValue(typeof(float), "0")]
        public virtual float RotationX
        {
            get { return rotationXVal; }
            set { rotationXVal = value; }
        }
        [DisplayName("Y Rotation"),
        Description("Rotate model along Y."),
        DefaultValue(typeof(float), "0")]
        public virtual float RotationY
        {
            get { return rotationYVal; }
            set { rotationYVal = value; }
        }
        [DisplayName("Z Rotation"),
        Description("Rotate model along Z."),
        DefaultValue(typeof(float), "0")]
        public virtual float RotationZ
        {
            get { return rotationZVal; }
            set { rotationZVal = value; }
        }

        #endregion

        #region Fields
        private ModelContent baseContent = null;
        private List<MorphInfo> morphList = new List<MorphInfo>();

        // for morph to base vertex difference
        private const float delta = 0.000001f;
        #endregion

        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            //ModelContent modelContent = base.Process(input, context);

            OpaqueDataDictionary odd = new OpaqueDataDictionary();
            odd.Add("Scale", scaleVal);
            odd.Add("RotationX", rotationXVal);
            odd.Add("RotationY", rotationYVal);
            odd.Add("RotationZ", rotationZVal);
            ModelContent modelContent = context.Convert<TInput, ModelContent>(input, "ModelProcessor", odd);

            if (String.IsNullOrEmpty(morphName))
                throw new InvalidContentException("Morph identifier missing!");

            if (!String.IsNullOrEmpty(baseName))
            {
                string basePath = Path.GetFullPath(baseName);
                if (basePath.Contains(".obj"))
                    baseContent = context.BuildAndLoadAsset<NodeContent, ModelContent>
                        (new ExternalReference<NodeContent>(basePath), "ObjContentProcessor", odd, "ObjContentImporter");
                else if (basePath.Contains(".fbx"))
                    baseContent = context.BuildAndLoadAsset<NodeContent, ModelContent>
                        (new ExternalReference<NodeContent>(basePath), "ModelProcessor", odd, "FbxImporter");
                else if (basePath.Contains(".x"))       // untested but included here for completion
                    baseContent = context.BuildAndLoadAsset<NodeContent, ModelContent>
                        (new ExternalReference<NodeContent>(basePath), "ModelProcessor", odd, "XImporter");
                else
                    throw new InvalidContentException("Unsupported base mesh type.");

                foreach (ModelMeshContent baseMesh in baseContent.Meshes)
                {
                    bool found = false;
                    foreach (ModelMeshContent modelMesh in modelContent.Meshes)
                    {
                        if (String.Equals(baseMesh.Name, modelMesh.Name))
                        {
                            found = true;
                            break;
                        }
                    }
                    if (!found)
                        throw new InvalidContentException("Mesh not consistent with base model!");
                }
            }
            else
            {
                // allow an empty base name if morphName is Base
                if (morphName != "Base")
                    throw new InvalidContentException("Base mesh missing!");
            }

            foreach (ModelMeshContent mesh in modelContent.Meshes)
            {
                MorphInfo morphInfo = new MorphInfo(morphName);
                morphInfo.Mesh = mesh.Name;

                // process morph here
                ProcessMorph(mesh, ref morphInfo);      
                if (morphInfo.VertexList.Count > 0)
                    morphList.Add(morphInfo);
            }

            if (morphList.Count > 0)
            {
                // save memory by deleting unnecessary data
                modelContent.Tag = morphList;
                return modelContent.Tag as List<MorphInfo>;
            }
            throw new InvalidContentException("Morph contents not found!");
        }

        /// <summary>
        /// Process Morph data from Vertex Buffer
        /// NB: Even though vertex buffer is accessible per meshPart, the vertex data
        ///     is for the entire mesh. Hence to get vertex data per mesh part,
        ///     use vertexOffset to read vertex data.
        /// </summary>
        /// <param name="mesh">Mesh to extract morph data</param>
        /// <returns></returns>
        private void ProcessMorph(ModelMeshContent mesh, ref MorphInfo morphInfo)
        {
            List<int> intList = new List<int>();
            List<float> flList = new List<float>();

            // get baseMesh from baseContent
            ModelMeshContent baseMesh = null;
            foreach (ModelMeshContent m in baseContent.Meshes)
            {
                if (m.Name == mesh.Name)
                {
                    baseMesh = m;
                    break;
                }
            }

            // TODO: support multi-part meshes; what if different vertex buffers per meshpart?
            // also, if multi-part, indexlist goes back to zero on next meshpart
            for (int j = 0; j < mesh.MeshParts.Count; j++)
            {
                ModelMeshPartContent meshPart = mesh.MeshParts[j];
                int strideBytes = (int)meshPart.VertexBuffer.VertexDeclaration.VertexStride;
                int numVertices = meshPart.NumVertices;
                int vertexBufferLength = numVertices * strideBytes;

                // create an array of float
                float[] flArray = new float[vertexBufferLength / sizeof(float)];
                // fill the array with mesh vertex data
                for (int i = 0; i < flArray.Length; i++)
                    flArray[i] = BitConverter.ToSingle(meshPart.VertexBuffer.VertexData, 
                                                       meshPart.VertexOffset * strideBytes + i * sizeof(float));
                //throw new InvalidContentException(flArray[2].ToString()); // for DEBUG

                if (morphName == "Base")
                {
                    // populate morphInfo
                    if (!String.IsNullOrEmpty(baseName))
                    {
                        // check consistency with base mesh
                        if (baseMesh.MeshParts[j].NumVertices == numVertices)
                        {
                            morphInfo.IndexList.AddRange(intList);
                            morphInfo.VertexList.AddRange(flArray.ToList<float>());
                        }
                        else
                            throw new Exception("Base mesh not consistent with original mesh. " +
                                        baseMesh.MeshParts[j].NumVertices.ToString() + "!=" +
                                        meshPart.NumVertices.ToString());

                    }
                    else
                    {
                        // no base mesh to compare with, accept blindly
                        morphInfo.IndexList.AddRange(intList);
                        morphInfo.VertexList.AddRange(flArray.ToList<float>());
                    }
                }
                else
                {
                    // check consistency with base mesh
                    if (baseMesh.MeshParts[j].NumVertices == meshPart.NumVertices) 
                    {
                        // get baseMesh information
                        float[] baseArray = new float[numVertices * strideBytes / sizeof(float)];
                        for (int i = 0; i < baseArray.Length; i++)
                            baseArray[i] = BitConverter.ToSingle(baseMesh.MeshParts[j].VertexBuffer.VertexData,
                                                                 baseMesh.MeshParts[j].VertexOffset * strideBytes + i * sizeof(float));

                        // form a sparse mask array
                        for (int k = 0; k < flArray.Length; k++)
                        {
                            float diff = flArray[k] - baseArray[k];
                            if (diff * diff > delta)
                            {
                                intList.Add(k);
                                flList.Add(diff);
                            }
                        }
                        morphInfo.IndexList.AddRange(intList);
                        morphInfo.VertexList.AddRange(flList);
                    }
                    else
                        throw new InvalidContentException("Morph target is not consistent with base mesh. " +
                                        baseMesh.MeshParts[j].NumVertices.ToString() + "!=" +
                                        meshPart.NumVertices.ToString());
                }
            }

#if false
            // assume stride is consistent for all mesh part
            int strideBytes = (int)mesh.MeshParts[0].VertexBuffer.VertexDeclaration.VertexStride;
            int vertexBufferLength = mesh.MeshParts[0].VertexBuffer.VertexData.Length;
            throw new InvalidContentException(vertexBufferLength.ToString() + " " + strideBytes.ToString());

            // create an array of float
            float[] flArray = new float[vertexBufferLength / sizeof(float)];
            // fill array with mesh vertex data
            for (int i = 0; i < flArray.Length; i++)
                flArray[i] = BitConverter.ToSingle(mesh.MeshParts[0].VertexBuffer.VertexData, i * sizeof(float));

            // get baseMesh from baseContent
            ModelMeshContent baseMesh = null;
            foreach (ModelMeshContent m in baseContent.Meshes)
            {
                if (m.Name == mesh.Name)
                {
                    baseMesh = m;
                    break;
                }
            }
                        if (morphName == "Base")
            {
                // populate morphInfo
                if (!String.IsNullOrEmpty(baseName))
                {
                    // check consistency with base mesh
                    if (baseMesh.MeshParts[0].VertexBuffer.VertexData.Length == mesh.MeshParts[0].VertexBuffer.VertexData.Length)
                    {
                        morphInfo.IndexList = intList;
                        morphInfo.VertexList = flArray.ToList<float>();
                    }
                    else
                        throw new Exception("Base mesh not consistent with original mesh. " +
                                    baseMesh.MeshParts[0].VertexBuffer.VertexData.Length.ToString() + "!=" +
                                    mesh.MeshParts[0].VertexBuffer.VertexData.Length.ToString());
                }
                else
                {
                    // no base mesh to compare with, accept blindly
                    morphInfo.IndexList = intList;
                    morphInfo.VertexList = flArray.ToList<float>();
                }
            }
            else
            {
                // check if consistent with base mesh
                if (baseMesh.MeshParts[0].VertexBuffer.VertexData.Length == mesh.MeshParts[0].VertexBuffer.VertexData.Length)
                {
                    // get baseMesh information
                    float[] baseArray = new float[baseMesh.MeshParts[0].VertexBuffer.VertexData.Length / sizeof(float)];
                    for (int i = 0; i < baseArray.Length; i++)
                        baseArray[i] = BitConverter.ToSingle(baseMesh.MeshParts[0].VertexBuffer.VertexData, i * sizeof(float));

                    // form a sparse mask array
                    for (int j = 0; j < flArray.Length; j++)
                    {
                        float diff = flArray[j] - baseArray[j];
                        if (diff * diff > delta)
                        {
                            intList.Add(j);
                            flList.Add(diff);
                        }
                    }
                    morphInfo.IndexList = intList;
                    morphInfo.VertexList = flList;
                }
                else
                    throw new InvalidContentException("Morph target is not consistent with base mesh. " +
                                    baseMesh.MeshParts[0].VertexBuffer.VertexData.Length.ToString() + "!=" +
                                    mesh.MeshParts[0].VertexBuffer.VertexData.Length.ToString());

            }
#endif
        }
    }
}